#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

using Osh.Media;

namespace Osh.ProgDvb
{
	public class Timeline : ITimeline
	{
		#region Native API
		[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
		private static extern IntPtr GetParent(HandleRef hWnd);

		[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
		private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

		[DllImport("user32.dll", SetLastError = true)]
		private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
		
		[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
		private static extern int GetWindowText(IntPtr hWnd, [Out] StringBuilder lpString, int nMaxCount);

		[DllImport("user32.dll")]
		private static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount);

		[DllImport("user32.dll", ExactSpelling=true)]
		private static extern bool EnumChildWindows(IntPtr hwndParent, EnumChildrenCallback lpEnumFunc, HandleRef lParam);

		public delegate bool EnumChildrenCallback(IntPtr hwnd, IntPtr lParam);
		#endregion

		#region TimelineState
		public class TimelineState : ITimelineInfo
		{
			private int _maxValue;
			private int _currentPosition;
			private DateTime? _startTimeUtc;
			private DateTime? _endTimeUtc;
			private DateTime? _currentTimeUtc;
			private DateTime _capturedUtc;

			public static TimelineState Empty = new TimelineState(0, 0, null, DateTime.MinValue);

			private TimelineState(int maxValue, int currentPosition,
				StringBuilder currentTime, DateTime capturedUtc)
			{
				_maxValue = maxValue;
				_currentPosition = currentPosition;
				_capturedUtc = capturedUtc;

				if(currentTime != null && currentTime.Length == 8) // HH:mm:ss
				{
					int hh = ((int)currentTime[0] - (int)'0') * 10 + ((int)currentTime[1] - (int)'0');
					int mm = ((int)currentTime[3] - (int)'0') * 10 + ((int)currentTime[4] - (int)'0');
					int ss = ((int)currentTime[6] - (int)'0') * 10 + ((int)currentTime[7] - (int)'0');

					TimeSpan currentTimeLocal = new TimeSpan(hh, mm, ss);
					DateTime approximateCurrentTimeUtc = _capturedUtc.AddSeconds(-(maxValue - currentPosition));

					int timezoneOffsetMinutes = (int)Math.Round(
						approximateCurrentTimeUtc.TimeOfDay.Subtract(currentTimeLocal).TotalSeconds / 60, 0);
					
					_currentTimeUtc = approximateCurrentTimeUtc.Date.Add(currentTimeLocal)
						.AddMinutes(timezoneOffsetMinutes);
					_startTimeUtc = _currentTimeUtc.Value.AddSeconds(-_currentPosition);
					_endTimeUtc = _currentTimeUtc.Value.AddSeconds(_maxValue - _currentPosition);
				}
			}

			internal int MaxValue
			{
				get { return _maxValue; }
			}

			internal int CurrentPosition
			{
				get { return _currentPosition; }
			}

			#region ITimelineInfo Members
			public DateTime? StartTime
			{
				get
				{
					if(_startTimeUtc == null)
						return null;
					return _startTimeUtc.Value.ToLocalTime();
				}
			}

			public DateTime? StartTimeUtc
			{
				get { return _startTimeUtc; }
			}

			public DateTime? EndTime
			{
				get
				{
					if(_endTimeUtc == null)
						return null;
					return _endTimeUtc.Value.ToLocalTime();
				}
			}

			public DateTime? EndTimeUtc
			{
				get { return _endTimeUtc; }
			}

			public DateTime? CurrentTime
			{
				get
				{
					if(_currentTimeUtc == null)
						return null;
					return _currentTimeUtc.Value.ToLocalTime();
				}
			}

			public DateTime? CurrentTimeUtc
			{
				get { return _currentTimeUtc; }
			}

			public DateTime Captured
			{
				get { return _capturedUtc.ToLocalTime(); }
			}

			public DateTime CapturedUtc
			{
				get { return _capturedUtc; }
			}
			#endregion ITimelineInfo Members

			public static TimelineState Capture(Timeline timeline)
			{
				int maxValue = Win32.SendMessage(timeline._trackBar, Win32.TBM_GETRANGEMAX, IntPtr.Zero, IntPtr.Zero).ToInt32();
				int currentPosition = Win32.SendMessage(timeline._trackBar, Win32.TBM_GETPOS, IntPtr.Zero, IntPtr.Zero).ToInt32();
				StringBuilder sb = new StringBuilder(100);				
				int result = GetWindowText(timeline._timelineWindow, sb, sb.Capacity);
				return new TimelineState(maxValue, currentPosition, sb, DateTime.UtcNow);
			}

			public static bool HasChanged(ITimelineInfo info1, ITimelineInfo info2)
			{
				if(object.ReferenceEquals(info1, info2))
					return false;
				
				if(info1 == null || info2 == null)
					return false;

				if(info1.CapturedUtc == info2.CapturedUtc)
					return false;

				return info1.CurrentTimeUtc != info2.CurrentTimeUtc ||
					info1.EndTimeUtc != info2.EndTimeUtc ||
					info1.StartTimeUtc != info2.StartTimeUtc;
			}
		}
		#endregion TimelineState

		private const int CurrentStateCacheInterval = 500; // ms
		private const int CaptureStateInterval = 1000; // ms

		private ProgDvbApplication _progDvbApp;
		private IntPtr _timelineWindow = IntPtr.Zero;
		private IntPtr _trackBar = IntPtr.Zero;
		private TimelineState _currentState;
		private System.Threading.Timer _timer;
		private int _insideOnTimeout = 0;

		internal Timeline(ProgDvbApplication progDvbApp)
		{
			if(progDvbApp == null)
				throw new ArgumentNullException("progDvbApp");
			_progDvbApp = progDvbApp;
			_progDvbApp.Start += new EventHandler(ProgDvbApp_Start);
			_progDvbApp.Exit += new EventHandler(ProgDvbApp_Exit);

			_timer = new Timer(new TimerCallback(this.OnTimeout));
		}

		private void ProgDvbApp_Start(object sender, EventArgs e)
		{
			_timer.Change(0, CaptureStateInterval);
		}

		private void ProgDvbApp_Exit(object sender, EventArgs e)
		{
			_timer.Change(Timeout.Infinite, Timeout.Infinite);
		}

		private void OnTimeout(object stateInfo)
		{
			if(Interlocked.CompareExchange(ref _insideOnTimeout, 1, 0) == 1)
				return;

			try
			{
				TimelineState prevState = _currentState;
				TimelineState state = this.CurrentState;
				if(this.Change != null && TimelineState.HasChanged(state, prevState))
					this.Change(this, new TimelineChangeArgs(state));
			}
			finally
			{
				Interlocked.CompareExchange(ref _insideOnTimeout, 0, 1);
			}
		}
		
		private bool EnsureWindows()
		{
			if(_timelineWindow != IntPtr.Zero)
			{
				if(Win32.IsWindow(_timelineWindow))
					return true;

				_timelineWindow = IntPtr.Zero;
				_trackBar = IntPtr.Zero;
			}

			return FindWindows();
		}

		private bool FindWindows()
		{
			IntPtr progDvbWin = FindWindow(null, "ProgDVB");
			if(progDvbWin == IntPtr.Zero)
				return false;

			_trackBar = FindWindowRecursive(progDvbWin, "msctls_trackbar32", "Slider1");
			if(_trackBar == IntPtr.Zero)
				return false;

			IntPtr trackBarParent = GetParent(new HandleRef(null, _trackBar));

			_timelineWindow = FindWindowEx(trackBarParent, IntPtr.Zero, "Static", null);
			if(_timelineWindow == IntPtr.Zero)
				return false;

			return true;
		}

		private IntPtr FindWindowRecursive(IntPtr hWin, string className, string winName)
		{
			if(className == null)
				className = string.Empty;
			if(winName == null)
				winName = string.Empty;

			IntPtr foundWnd = IntPtr.Zero;
			EnumChildWindows(hWin, new EnumChildrenCallback(
				delegate(IntPtr hWnd, IntPtr lParam)
				{
					StringBuilder winNameSb = new StringBuilder(Math.Max(100, className.Length + 1));
					int length = GetWindowText(hWnd, winNameSb, winNameSb.Capacity);
					if(length != winName.Length || winNameSb.ToString() != winName)
						return true;

					StringBuilder classNameSb = new StringBuilder(Math.Max(100, className.Length + 1));
					length = GetClassName(hWnd, classNameSb, classNameSb.Capacity);
					if(length != className.Length || classNameSb.ToString() != className)
						return true;

					foundWnd = hWnd;
					return false;
				}), new HandleRef());
			return foundWnd;
		}

		public TimelineState CurrentState
		{
			get
			{
				if(_currentState == null ||
					_currentState.CapturedUtc.AddMilliseconds(CurrentStateCacheInterval) < DateTime.UtcNow)
				{
					if(EnsureWindows())
						_currentState = TimelineState.Capture(this);
					else
						_currentState = TimelineState.Empty;
				}
				return _currentState;
			}
		}

		/// <summary>
		/// Ensures that timeline properties return up to date information.
		/// Call this method to make sure that timeline information read in a short period of time
		/// after this method completes is in sync.
		/// </summary>
		public void Refresh()
		{
			_currentState = null;
		}

		public bool GetTimeInfo(out DateTime startTime, out DateTime currentTime, out DateTime endTime)
		{
			TimelineState state = TimelineState.Capture(this);
			startTime = state.StartTimeUtc.GetValueOrDefault();
			currentTime = state.CurrentTimeUtc.GetValueOrDefault();
			endTime = state.EndTimeUtc.GetValueOrDefault();
			return state.StartTimeUtc.HasValue && state.CurrentTimeUtc.HasValue && state.EndTimeUtc.HasValue;
		}

		#region ITimeline Members
		public ITimelineInfo CaptureState()
		{
			if(EnsureWindows())
				return TimelineState.Capture(this);
			else
				return TimelineState.Empty;
		}

		public bool CanSeek
		{
			get { return true; }
		}

		public DateTime? SeekTo(DateTime newPosition)
		{
			DateTime? startTime = this.CurrentState.StartTime;
			if(startTime == null)
				return null;
			TimeSpan timeOffset = newPosition.Subtract(startTime.Value);
			int trackBarOffset = (int)timeOffset.TotalSeconds;
			if(trackBarOffset < 0)
				trackBarOffset = 0;

			IntPtr redrawFlag = new IntPtr(1); // redraw the slider
			IntPtr mousePosition = new IntPtr(0xA0014); // x = 0x14, y = 0xA
			Win32.SendMessage(_trackBar, Win32.WM_LBUTTONDOWN, new IntPtr(Win32.MK_LBUTTON), mousePosition);
			Win32.SendMessage(_trackBar, Win32.TBM_SETPOS, redrawFlag, new IntPtr(trackBarOffset));
			Win32.SendMessage(_trackBar, Win32.WM_LBUTTONUP, new IntPtr(Win32.MK_LBUTTON), mousePosition);

			// Get new position
			IntPtr position = Win32.SendMessage(_trackBar, Win32.TBM_GETPOS, IntPtr.Zero, IntPtr.Zero);
			return startTime.Value.AddSeconds(position.ToInt32());
		}

		public DateTime? SeekBy(int msOffset)
		{
			return this.SeekBy(TimeSpan.FromMilliseconds(msOffset));
		}

		public DateTime? SeekBy(TimeSpan offset)
		{
			DateTime? currentTime = this.CurrentState.CurrentTime;
			if(currentTime == null)
				return null;
			return this.SeekTo(currentTime.Value.Add(offset));
		}

		public event EventHandler<TimelineChangeArgs> Change;
		#endregion ITimeline Members
	}
}
